
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;

public class PlayerSkeleton {
        private static String filename;
        public static int selectingIndex = 0;
        public static double start = 0.5, end = 20, stepSize = 1;

        public static int seed = 0;
	public static double    numOfUnitClearedPara,
                                colDifPara,
                                maxHeightOfHolePara,
                                landingHeightPara,
                                sumOfHolesPara,
                                sumOfWellsPara,
                                rowsWithHolesPara,
                                maxWellPara,
                                holeNegMatchPara,
                                holePosMatchPara;
	

	// implement this function to have a working system
	public int pickMove(State s, int[][] legalMoves) {

		// final move for current piece
		int bestMove = 0; 

		// array of all the heuristic values for all possible moves
		double[] hValues = new double[legalMoves.length];

		// predicted number of rows cleared
		int rowsCleared = 0;

		int[][] sField = new int[21][10];

		// start simulating each possible move 
		for(int i = 0; i < legalMoves.length; i++) {

			// duplicate field to simulate the array
			sField = duplicateField(s.getField());

			// duplicate the top[] array
			int[] sTop = duplicateTop(s.getTop());

			int pTurn 		 = s.getTurnNumber();
			int pOrientation = legalMoves[i][0];
			int pSlot 		 = legalMoves[i][1];
			int [][] pBottom = s.getpBottom()[s.getNextPiece()];
			int [][] pTop 	 = s.getpTop()[s.getNextPiece()];
			int [] pHeight 	 = s.getpHeight()[s.getNextPiece()];
			int [] pWidth 	 = s.getpWidth()[s.getNextPiece()];

			// get the height of the slot before simulated move
			int sHeight  = simulateHeight(sTop, pBottom, pWidth, pOrientation, pSlot);  

			// check whether current move will result in loss or not, if no then we simulate the move
			if(!checkFieldStatus(sHeight, pHeight, pOrientation, pSlot)) {

				/** 
				 * Simulate the move, this method will not clear any full rows yet
				 * take note turn number before move is i, so turn number during
				 * simulated turn is i+1
				 */
				sField = simulateMove(sField, i, sHeight, pTurn, sTop, pBottom, pTop, pWidth, pHeight, pOrientation, pSlot);  

				rowsCleared = this.predictedRowsClear(sField, sHeight, pHeight, sTop, pTop, pWidth, pOrientation, pSlot);

				// we then clear the full rows
				clearRows(sField, sHeight, sTop, pHeight, pOrientation, pSlot);

				// from the new simulated field, we check the heuristic value
				hValues[i] = heuristic(sField, pTurn+1, rowsCleared); 
			}
			// move results in a loss, so heuristic value of move set to 0.
			else {
				hValues[i] = Double.NEGATIVE_INFINITY;
			}
		}

		// after all the heuristic values are determined, find the best move
		double maxHValue = Double.NEGATIVE_INFINITY; 
		for(int i = 0; i < hValues.length; i++) {
			if(hValues[i] >= maxHValue) {
				maxHValue = hValues[i];
				bestMove = i;
			} 
		}
		// System.out.println("best move is "+ bestMove + " for piece number "+ s.getNextPiece());
		// printField(sField); // this will only show the field before the move

		return bestMove;
	}

	public int simulateHeight(int[] top, int[][] pBottom, int[] pWidth, int orient, int slot) {

		// height if the first column makes contact
		int height = top[slot] - pBottom[orient][0];

		// for each column beyond the first in the piece
		for(int c = 1; c < pWidth[orient]; c++) {
			height = Math.max(height, top[slot+c] - pBottom[orient][c]);
		} 

		return height;
	}

	/**
	 * Clear any full rows in the simulated field
	 */
	public void clearRows(int[][] field,int height, int[] top, int[] pHeight,int orient,int slot) {

		// check for full rows - starting at the top
		for(int r = height + pHeight[orient] - 1; r >= height; r--) {

			//check all columns in the row
			boolean full = true;
			for(int c = 0; c < 10; c++) {
				if(field[r][c] == 0) {
					full = false;
					break;
				}
			}

			//if the row was full - remove it and slide above stuff down
			if(full) {

				//for each column
				for(int c = 0; c < 10; c++) {
					//slide down all bricks
					for(int i = r; i < top[c]; i++) {
						field[i][c] = field[i+1][c];
					}

					//lower the top
					top[c]--;
					while(top[c] >= 1 && field[top[c]-1][c] == 0) 
						top[c]--;
				}
			}
		}
	}

	/**
	 * Stub for the general heuristic value for 1 move.
	 * will consists of all the heuristics methods in this general method
	 */
	public double heuristic(int[][] sField, int turn, int rowsCleared) {

		double heuristic = 0;
		int[] colHoleHeight = new int[10];

		// find all column heights
		int[] colHeights = this.colHeightHeur(sField);

		// max column height
		int maxColHeight = this.maxColHeight(colHeights);

		// min column height
		int minColHeight = this.minColHeight(colHeights), colDif = maxColHeight - minColHeight;

		// estimated landing height of move
		int landingHeight = this.landingHeight(sField, turn);

		// difference between adjacent column heights
		int[] adjHeightDifference = this.findAdjHeightDifference(colHeights);//

		int sumOfHoles = this.findNumberOfHoles(sField, colHoleHeight );

		int sumOfBlocksOverHoles = this.findSumOfBlocksOverHoles(sField, colHoleHeight);//

		int maxHeightOfHole = this.getHeightOfHighestHole( colHoleHeight );

		int sumOfConnectingHorizontalHole = this.getNoOfConnectHole(sField, colHeights);//

		int rowsWithHoles = rowsWithHoles(sField);

		int numOfUnitCleared = rowsCleared * 10;

		int[] wellsArray = this.wellsArray(colHeights);

		int maxWell = this.maxWellDepth(wellsArray);

		int sumOfWells = 0;
		for(int i = 0; i < wellsArray.length; i++) {
			sumOfWells += wellsArray[i];
		}

		int pieceNegativeMatch = this.getHoleNegativity(sField);
		int piecePositiveMatch = this.getNewPieceMatchness(sField, turn);
		
		/** Main heuristic:
	     *   landingHeightPara 	= -12.63;
         *	 sumOfHolesPara 	= -13.08;
         *	 sumOfWellsPara 	= -11.39;
         *	 rowsWithHolesPara 	= -24.2;
         *	 maxWellPara 		= -5.4;
         *	 holeNegMatchPara 	= -1;
         *	 holePosMatch 		= 1;
		 */
		heuristic =        numOfUnitClearedPara         * numOfUnitCleared +
                                        colDifPara              * colDif +
                                        maxHeightOfHolePara     * maxHeightOfHole +
                                        landingHeightPara       * landingHeight +
					sumOfHolesPara          * sumOfHoles +
					sumOfWellsPara          * sumOfWells +
					rowsWithHolesPara       * rowsWithHoles +
					maxWellPara             * maxWell +
					holeNegMatchPara        * pieceNegativeMatch +
					holePosMatchPara        * piecePositiveMatch;
		
		/*
        if(averageHeight<15  && altitudeDiff<8 && sumOfWells<4) {
          	heuristic = -20*sumOfHoles - landingHeight + 4.0*rowsCleared;
          	System.out.println("OK");
        }
        else {
          	heuristic = - 12.63 * landingHeight - 13.08 * sumOfHoles -1.09*sumOfWells -3.2*rowsWithHoles -5.4*maxWell;
          	System.out.println("D");
        }
		 
		best so far can hit 1200 heuristic = - 12.63 * landingHeight - 13.08 * sumOfHoles -1.39*sumOfWells -3.2*rowsWithHoles -5.4*maxWell;
		*/
		
		return heuristic; 
	}

	/**
	 * Helper methods to print field and other data structures
	 */ 
	public void printColHeights(int[] colHeights) {
		System.out.println("Printing col heights in process");
		for(int i = 0; i < colHeights.length; i++) {
			System.out.print("[" + colHeights[i] + "]");
		}
	}

	public void printField(int[][] field) {
		System.out.println("Printing field in process");
		for(int i = field.length - 1; i >= 0; i--) {
			System.out.print("row " + i);
			for(int j = 0;j < field[i].length; j++) {
				System.out.print("["+field[i][j]+"]");
			}
			System.out.println();
		}
	}

	/**
	 * Bonus heuristics for number of rows cleared in simulated field
	 */
	public int predictedRowsClear(int[][] field, int height,int[] pHeight, int[] top, int[][] pTop, int[] pWidth, int orient, int slot) {

		int rowsCleared = 0;
		int[][] dField = duplicateField(field);

		Boolean full = true;

		for(int r = height + pHeight[orient] - 1; r >= height; r--) {

			//check all columns in the row
			full = true;

			for(int c = 0; c < 10; c++) {    
				if(dField[r][c] == 0) {
					full = false;
					break;
				}
			}   
			if(full) 
				rowsCleared++;
		}
		return rowsCleared;
	}

	/**
	 * Basically this code is similar to the makeMove method in State.java in which the height will be bigger than 21 if the move is made
	 * from line 193 to line 204
	 */
	public boolean checkFieldStatus(int height, int[] pHeight, int orient, int slot) {
		boolean lost = false;

		// check if game ended
		if(height + pHeight[orient] >= 21) {
			lost = true;
		}
		return lost;
	}

	/**
	 * This is to simulate the move, but rows that are full will not be cleared in this method.
	 * Same as makeMove method in State.java from line 190 to 215. We then check the number of rows
	 * possibly cleared for this move, then we clear the full rows and perfrom the heuristic function.
	 */
	public int[][] simulateMove(int[][] field, int move,int height,int turn,int[] top, int[][] pBottom,int[][] pTop, int[] pWidth,int[] pHeight, int orient,int slot){

		turn++;

		for(int i = 0; i < pWidth[orient]; i++) {
			for(int h = height + pBottom[orient][i]; h < height + pTop[orient][i]; h++) {
				field[h][i+slot] = turn;
			}
		}

		for(int c = 0; c < pWidth[orient]; c++) {
			top[slot+c] = height + pTop[orient][c];
		}

		return field;
	}

	/**
	 * Duplicated the top array. this array changes with every move, hence the need to duplicate it before simulated move
	 */
	public int[] duplicateTop(int[] top) {
		int[] dTop = new int[10];

		for(int i = 0;i < top.length; i++) {
			dTop[i] = top[i];
		}

		return dTop;
	}

	/**
	 * Duplicate the field. since the field changes after every move, hence the need to duplicate it before simulated move.
	 */
	public int[][] duplicateField(int[][] field) {
		int[][] dField = new int[21][10];

		for(int i = 0; i < field.length; i++) {
			for(int j = 0; j < field[i].length; j++) {
				dField[i][j] = field[i][j];
			}
		}
		return dField;
	}

	/**
	 * Finding the minimum of all column heights
	 */
	public int minColHeight(int[] colHeights) {
		int minColHeight = 21;

		for(int i = 0; i < colHeights.length; i++) {
			if(colHeights[i] < minColHeight)
				minColHeight = colHeights[i];
		}

		return minColHeight;
	}

	/**
	 * Finding the maximum of all column heights
	 */
	public int maxColHeight(int[] colHeights) {
		int maxColHeight = 0;

		for(int i = 0; i < colHeights.length; i++) {
			if(colHeights[i] >= maxColHeight)
				maxColHeight = colHeights[i];
		}

		return maxColHeight;
	}

	/**
	 * Finding all column heights and return as an array
	 */
	public int[] colHeightHeur(int[][] field) {
		int[] colHeight = new int[10];

		for(int i = 0;i < colHeight.length; i++) {
			colHeight[i] = 0;
		}

		for(int j = 0; j < 10; j++) {       					// field[i].length is always 10
			for(int i = field.length - 1; i >= 0; i--) {    		// field.length is 21
				if(field[i][j] != 0 && colHeight[j] == 0) {
					colHeight[j] = i+1;
					break;
				}
			}
		}
		
		return colHeight;
	}

	/**
	 * Find the number of rows with at least one hole
	 */
	public int rowsWithHoles(int[][] field) {
		int rows = 0;
		for(int i = 0; i < field.length - 1; i++){
			for(int j = 0; j < field[i].length; j++){
				if(field[i][j] == 0 && field[i+1][j] != 0){
					rows++;
					break;
				}
			}
		}
		//System.out.println(rows);
		return rows;
	}

	/**
	 * Finding the estimated landing height. 
	 */
	public int landingHeight(int[][] field, int turn) {
		int landingHeight = 0;

		outerloop:{
			for(int i = field.length - 1; i >= 0; i--) {
				for(int j = 0; j < 10; j++) {
					if(field[i][j] == turn) {
						landingHeight = i+1;
						break outerloop;
					}
				}
			}
		}

		return landingHeight;
	}

	/**
	 * Find the absolute difference between adjacent column heights
	 */
	public int[] findAdjHeightDifference(int[] colHeights ) {
		int[] adjHeightDifference = new int[colHeights.length - 1];

		for (int i = 0; i < adjHeightDifference.length; i++) {
			adjHeightDifference[i] = Math.max(colHeights[i], colHeights[i+1]) - Math.min(colHeights[i], colHeights[i+1]);
		}

		return adjHeightDifference;
	}

	/** 
	 * Takes input arg from colHeightHeur()
	 * return array of wells  (if value>0 -> depth=value) else (not a well)
	 */
	public int[] wellsArray(int[] colHeight) {
		int[] wells = new int[colHeight.length];

		wells[0] = colHeight[1] - colHeight[0];
		wells[colHeight.length - 1] = colHeight[colHeight.length - 2] - colHeight[colHeight.length - 1];

		for(int i = 1; i < colHeight.length - 2; i++) {

			// definition of well: distance between two walls are more than one
			if((colHeight[i-1] - colHeight[i]) > 1 && (colHeight[i+1] - colHeight[i]) > 1) {
				if(colHeight[i+1] > colHeight[i-1])
					wells[i]= colHeight[i-1] - colHeight[i];
				else
					wells[i]= colHeight[i+1] - colHeight[i];
			}
		}
		for(int i=0;i<wells.length;i++) {
			if(wells[i]<0)
				wells[i]=0;
		}

		return wells;
	}

	/**
	 * Takes input arg from wellsArray(int[] colHeight)
	 * return COL maxWellDepth, if there's no well return 0
	 */ 
	public int maxWellDepth(int[] wellsArray) {
		int maxDepth = Integer.MIN_VALUE;

		for(int i = 1; i < wellsArray.length - 1; i++) {
			if(maxDepth < wellsArray[i])
				maxDepth = wellsArray[i];
		}

		if(maxDepth > 1) 
			return maxDepth;
		else 
			return 0;
	}

	/** 
	 * Takes input arg from wellsArray(int[] colHeight)
	 */ 
	public int numberOfWells(int[] wellsArray) {
		int maxDepth = 0;

		for(int i = 1; i < wellsArray.length-1; i++) {
			if(wellsArray[i] > 1)
				++maxDepth;
		}

		return maxDepth;
	}

	/**
	 * Find number of hole(unoccupied cell) found in field
	 */
	public int findNumberOfHoles(int[][] field, int[] holeHeights) {
		int result = 0;

		for(int i = 0 ; i < field[0].length; i++) {
			holeHeights[i] = 0;
			for(int j = 20; j >= 0; j--) {
				if (field[j][i] != 0) {
					for(int k = --j; k >= 0; k--)
						if (field[k][i] == 0) {
							if (holeHeights[i] < k) 
								holeHeights[i] = k;
							result++;
						}

					break;
				}
			}
		}
		return result;
	}

	/**
	 * 
	 */
	private int findSumOfBlocksOverHoles(int[][] field, int[] holeHeights) {
		int result = 0;

		for(int i = 0; i < field[0].length; i++) {
			for(int j = holeHeights[i] + 1; j < field.length; j++) {
				if (field[j][i] != 0)
					result++;
				else 
					break;
			}
		}      
		return result;
	}

	/**
	 * 
	 */
	public int getHeightOfHighestHole( int[] holeHeights ) {
		int max = 0;
		for(int i = 0 ; i < holeHeights.length; i++)
			if (max < holeHeights[i] )  
				max = holeHeights[i];
		return max;
	}

	/**
	 * 
	 */
	public int getNoOfConnectHole(int[][] field, int[] holeHeights) {
		int result = 0;

		for(int i = 0 ; i < field[0].length - 1; i++) {
			for(int j = holeHeights[i]; j >= 0; j-- ) {
				if (j <= holeHeights[ i+1 ] && field[j][i] == 0 && field[j][i + 1] == 0) {
					result++;
				}
			}
		} 
		return result;
	}

	/**
	 * check to get number of occupied blocks surround by hole
	 */
	private int getHoleNegativity(int[][] field) {
		int sum = 0;
		boolean check;
		for( int x = 0; x < 10; x ++ ) {
			check = false;
			for( int y = 20; y > 0; y -- )
				if (check && field[y][x] == 0 ) {
					sum += countPieceSuroundingBlock(field, x, y, 0, false);
				}
				else if (!check && field[y][x] > 0)
					check = true;
		}
		return sum;
	}
	
	private int countPieceSuroundingBlock(int[][] field, int x, int y, int exclusingPiece,boolean includeBorder) {
		int sum = 0;

		if ( (x-1 > 0  && field[y][x-1] > 0 && field[y][x-1] != exclusingPiece) || (includeBorder && x -1 < 0) )
                        sum++;
		if ( (x+1 < 10 && field[y][x+1] > 0 && field[y][x+1] != exclusingPiece) || (includeBorder && x + 1 >= 10) )
                    sum++;
		if ( (y-1 > 0  && field[y][x]   > 0 && field[y-1][x] != exclusingPiece) || (includeBorder && y -1 < 0) )
                    sum++;
		if (  y+1 < 20 && field[y][x]   > 0 && field[y+1][x] != exclusingPiece )
                    sum++;
		return sum;
	}
	
	private int getNewPieceMatchness(int[][] field, int turn ) {
		int sum = 0;
		for(int y = 0; y < 20; y++)
			for(int x = 0; x < 10; x++)
				if (field[y][x] == turn) {
					sum += countPieceSuroundingBlock(field, x, y, turn,true);
				}
		return sum;
	}

	public static void main(String[] args) {
                filename = System.currentTimeMillis() + " - result.csv";
                fileLogger( 0, true);

		
		PlayerSkeleton p = new PlayerSkeleton();

                //Default values //
                numOfUnitClearedPara    = 1.5;//1.5 with 6 holepos
                colDifPara              = -1.5;
                maxHeightOfHolePara     = -2;
                landingHeightPara 	= -12.63;
                sumOfHolesPara 		= -13.08;
                sumOfWellsPara 		= -11.39;
                rowsWithHolesPara 	= -28.2;	// 28.2
                maxWellPara 		= -5.4;
                holeNegMatchPara 	= -12.45;
                holePosMatchPara 	= 6;//2.25  << 2.24 gd // 6best

		//int seed = 0;
                if (args.length == 1) {
                    String line =  args[0]+ ",,,";
                    String token[] = line.split( ",",4 );
                    token[3] = token[3].replace(",", "");
                    selectingIndex  = Integer.parseInt(token[0]);
                    start           = Double.parseDouble(token[1]);
                    end             = Double.parseDouble(token[2]);
                    stepSize        = Double.parseDouble(token[3]);
                }
                else if(args.length >= 11) {
			seed = Integer.parseInt(args[0]);
			numOfUnitClearedPara    = Double.parseDouble(args[1]);	// 2.0;
                        colDifPara              = Double.parseDouble(args[2]);	// -1.5;
                        maxHeightOfHolePara     = Double.parseDouble(args[3]);	// -2;
                        landingHeightPara 	= Double.parseDouble(args[4]);	// -12.63;
			sumOfHolesPara 		= Double.parseDouble(args[5]);	// -13.08;
			sumOfWellsPara 		= Double.parseDouble(args[6]);	// -11.39;
			rowsWithHolesPara 	= Double.parseDouble(args[7]);	// -24.2;
			maxWellPara 		= Double.parseDouble(args[8]);	// -5.4;
			holeNegMatchPara 	= Double.parseDouble(args[9]);	// -1;
			holePosMatchPara 	= Double.parseDouble(args[10]);	// 1;

                        selectingIndex  = 0;
                        start           = 1;
                        end             = 1;
                        stepSize        = 1;
		}
		else {

                        getUserSelection();
		}	// 2.45 - 29300; 12.45 -> ;

                
                for( double value = start; value <= end; value += stepSize )
                {
                    State s = new State();                  //create new game state
                    TFrame frame = new TFrame(s);           //create new form
                    setParameter( selectingIndex, value );  //reset heruistic parameters
                    s.setRandomSeed(seed);                  //Reset seed no
                    oneCycle( s , p );                      //process one cycle
                    fileLogger( s.getRowsCleared(), false); //log result
                    frame.dispose();                        //Dispose old form
                    s = null;                               //unsigned old state
                }
		System.out.println( seed + " " + landingHeightPara + " " + sumOfHolesPara + " " + sumOfWellsPara + " " + rowsWithHolesPara + " " + maxWellPara + " " + holeNegMatchPara + " " + holePosMatchPara);
	
		System.exit(0) ;
	}

        public static void fileLogger( int result, boolean header )
        {
            System.out.println("You have completed "+ result+" rows.");

            File   actLog = new File( filename );
            FileWriter actLogger;
            try {
                actLogger = new FileWriter(actLog, true); // true to append   ;false to overwrite
                if ( header )
                {
                    actLogger.write( "Result, ," );
                    actLogger.write( "seed" + "," );
                    actLogger.write( "numOfUnitClearedPara" + "," );
                    actLogger.write( "colDifPara" + "," );
                    actLogger.write( "maxHeightOfHolePara" + "," );
                    actLogger.write( "landingHeightPara" + "," );
                    actLogger.write( "sumOfHolesPara" + "," );
                    actLogger.write( "sumOfWellsPara" + "," );
                    actLogger.write( "rowsWithHolesPara" + "," );
                    actLogger.write( "maxWellPara" + "," );
                    actLogger.write( "holeNegMatchPara" + "," );
                    actLogger.write( "holePosMatchPara" + "\n" );
                }
                else
                {
                    actLogger.write( result + ",," );
                    actLogger.write( seed + "," );
                    actLogger.write( numOfUnitClearedPara + "," );
                    actLogger.write( colDifPara + "," );
                    actLogger.write( maxHeightOfHolePara + "," );
                    actLogger.write( landingHeightPara + "," );
                    actLogger.write( sumOfHolesPara + "," );
                    actLogger.write( sumOfWellsPara + "," );
                    actLogger.write( rowsWithHolesPara + "," );
                    actLogger.write( maxWellPara + "," );
                    actLogger.write( holeNegMatchPara + "," );
                    actLogger.write( holePosMatchPara + "\n" );
                }
                actLogger.close();

            } catch (IOException ex) {
                
                System.err.print("Fail to open file - " + filename );
                System.exit(-99);
            }
        }

        public static void setParameter(int  index, double value )
        {
            switch( index )
            {
                case 0:         seed = (int)value;                  break;
                case 1:         numOfUnitClearedPara    = value;    break;
                case 2:         colDifPara              = value;    break;
                case 3:         maxHeightOfHolePara     = value;    break;
                case 4:         landingHeightPara 	= value;    break;
		case 5: 	sumOfHolesPara 		= value;    break;
		case 6: 	sumOfWellsPara 		= value;    break;
		case 7: 	rowsWithHolesPara 	= value;    break;
		case 8: 	maxWellPara 		= value;    break;
		case 9: 	holeNegMatchPara 	= value;    break;
		case 10: 	holePosMatchPara 	= value;    break;
            }

        }
        private static void oneCycle( State s, PlayerSkeleton p )
        {
            while(!s.hasLost()) {
                    s.makeMove(p.pickMove(s,s.legalMoves()));
                    //s.draw();
                    //s.drawNext(2,0);
                    /*
                    try {
                            Thread.sleep(0);
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }*/
		}

        }
        public static void getUserSelection()
        {
            System.out.print("Please enter a testing parameter index (0-10), min value, max value, step size: [index, min, max, step]\n");
            InputStreamReader converter = new InputStreamReader(System.in);
            BufferedReader in = new BufferedReader(converter);
            try {
                String line = in.readLine() + ",,,";
                String token[] = line.split( ",",4 );
                token[3] = token[3].replace(",", "");

                selectingIndex  = Integer.parseInt(token[0]);
                start           = Double.parseDouble(token[1]);
                end             = Double.parseDouble(token[2]);
                stepSize        = Double.parseDouble(token[3]);

            } catch (IOException ex) {
                Logger.getLogger(PlayerSkeleton.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
}
